package com.cskaoyan.mall.gateway.filter;

import com.alibaba.fastjson.JSONObject;
import com.cskaoyan.mall.common.constant.ResultCodeEnum;
import com.cskaoyan.mall.common.result.Result;
import com.cskaoyan.mall.common.util.IpUtil;
import com.cskaoyan.mall.user.consts.UserConstants;
import com.cskaoyan.mall.user.dto.UserLoginInfoDTO;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @Author Pudding
 * @Date 2024/7/13 22:21
 * 实现全局过滤器的接口，那么这个过滤器就会拦截所有的请求
 * 在这里，我们是需要实现登录校验的逻辑
 * 补充: 什么样的接口需要登录，什么样的接口不需要登录呢？
 */
// 接口路径格式为: /*/auth/** 需要登录, 不满足这个格式那么就不需要登录
@Component
public class AuthFilter implements GlobalFilter {

    @Value("${authUrls.url}")
    private String authUrl;  //    /*/auth/**
    @Autowired
    RedissonClient redissonClient;

    //创建一个springmvc路径匹配工具
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //1. 判断当前访问的接口是否需要登录
        //1.1 获取当前用户的请求路径
        String path = request.getURI().getPath(); //从HTTP请求中提取出URI的路径部分，并将其存储在path变量中
        //1.2 根据请求路径和配置的authUrl进行匹配
        boolean isNeedLogin = judgeNeedLogin(path, authUrl);

        //2. 判断当前用户是否已经登录
        String userId = hasLogin(request);
        //2.1 根据userId的状态判断ip是否相同进而判断账号请求是否不合法
        if ("-1".equals(userId)){
            return out(response, ResultCodeEnum.ILLEGAL_REQUEST);
        }
        //2.2 如果用户未登录且访问的接口需要登录，那么拦截
        if (userId == null && isNeedLogin){
            return out(response, ResultCodeEnum.LOGIN_AUTH);
        }

        //2.3 如果是其他的情况(访问的接口不需登录且用户没有登录)，或者是用户已经登录，那么放行
        //2.3.1 在放行之前额外做的事情1： 为了方便后续的下游的服务能够方便获取到用户id，我们可以把用户id放到请求头中，之后其他请求就不用再查询redis了
        //做法：因为所有请求都会经过网关，所有我们在网关中获取userId，并放入请求头中
        ServerHttpRequest.Builder builder = request.mutate();// 复制出一个请求头
        if (StringUtils.isNotBlank(userId)){
            builder.header("userId", userId); // 添加userId字段到这个请求头
        }

        //2.3.2 在放行之前额外做的事情2： 为了方便购物车的业务逻辑，可以也统一的把临时用户id放入请求头
        ServerWebExchange newExchange = getNewExchange(exchange, request, builder);

        //2.4 放行
        return chain.filter(newExchange);
    }

    private static ServerWebExchange getNewExchange(ServerWebExchange exchange, ServerHttpRequest request, ServerHttpRequest.Builder builder) {
        //2.3.2.1 把临时用户id从请求头/cookie中获取到
        String userTempId = request.getHeaders().getFirst("userTempId");
        if (StringUtils.isBlank(userTempId)){
            HttpCookie httpCookie = request.getCookies().getFirst("userTempId");
            if (httpCookie != null){
                userTempId = httpCookie.getValue();
            }
        }
        //2.3.2.2 临时用户id放入请求头
        if (StringUtils.isNotBlank(userTempId)){
            builder.header("userTempId", userTempId);
        }
        //2.3.2.3 构建出一个新的请求头
        ServerHttpRequest newRequest = builder.build();
        //2.3.2.4 把新的请求头放入新的exchange对象中
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
        return newExchange;
    }

    //根据请求路径和配置的authUrl进行匹配
    private boolean judgeNeedLogin(String path, String authUrl) {
        boolean result = antPathMatcher.match(authUrl, path);
        return result;
    }

    /**
     * 判断当前用户是否已经登录
     * @param request
     * @return
     *    null：说明用户没有登录
     *    "-1"：说明ip被盗用
     *    userId：说明用户之前已经登录
     */
    private String hasLogin(ServerHttpRequest request) {

        //1. 从请求中获取token的值
        //1.1 先从请求头中获取token的值
        String token = request.getHeaders().getFirst("token");
        //1.2 如果请求头中没有获取到，再从cookie中获取token的值
        if (StringUtils.isBlank(token)){
            HttpCookie httpCookie = request.getCookies().getFirst("token");
            if (httpCookie != null){
                token = httpCookie.getValue();
            }
        }

        //2. 如果token依然为空，那么说明请求中没有token，那么说明用户没有登录
        if (StringUtils.isBlank(token)){
            return null;
        }

        //3. 如果token不为空，那么说明用户已经登录，根据key去redis中查询用户信息
        //3.1 获取redis中存储的key
        String key = UserConstants.USER_LOGIN_KEY_PREFIX + token;
        //3.2 根据key去redis中查询用户信息
        RBucket<UserLoginInfoDTO> bucket = redissonClient.getBucket(key);
        UserLoginInfoDTO userLoginInfoDTO = bucket.get();
        //3.3 判断是否查询到用户信息
        if (userLoginInfoDTO == null) {
            return null;
        }

        //4. 判断用户登录的ip和当前请求的ip是否一致，如果不一致，说明ip被盗用
        //4.1 获取用户登录时的ip
        String ip = userLoginInfoDTO.getIp();
        //4.2 获取当前请求的ip
        String currentIp = IpUtil.getGatwayIpAddress(request);
        //4.3 判断ip是否一致，进而判断是否被盗用
        if (!ip.equals(currentIp)){
            return "-1";
        }

        //5. 查询到用户信息，且ip一致，返回userId
        return userLoginInfoDTO.getUserId();
    }

    //接口鉴权失败返回数据
    private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {

        //1. 返回用户没有权限的登录
        Result<Object> result = Result.build(null, resultCodeEnum);

        //2. 将result对象转化为JSON字符串，并将字符串转换为字节数据
        byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);

        //3. 封装一个字节数据为一个DataBuffer数据缓冲区
        DataBuffer wrap = response.bufferFactory().wrap(bits);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

        //4. 输出到页面，返回一个json字符串
        return response.writeWith(Mono.just(wrap));
    }
}
